import axios from 'axios'
// 全局配置项，导出到外部文件，更方便管理与代码复用
import * as defaultConfig from '@/services/config'

import { showLoading, hideLoading } from '@/components/GlobalModal'

const { CancelToken } = axios

/**
 * 封装一个axios类
 */
export default class Request {
  /**
   * 实例化时传入参数进行配置
   * @param {*} param config 实例化时传入的配置参数
   * @param {*} param [interceptors] 实例化时配置的拦截器对象
   *
   * @param {*} param [useGlobalLoding] 是否使用全局的Loading 默认为true
   * @param {*} param [errorBackCaller] 是否对response中的axios状态提前进行处理 默认为false
   * @param {*} param [useRetryRequest] 请求超时后是否需要自动重连 默认为true
   * @param {*} param [useRetryCount] useRetryRequest为true时有效，自动重连次数，超过次数则返回请求超时 默认3次
   * @param {*} param [useRetryDelay] useRetryRequest为true时有效，超时后自动重连等待时长 默认为1000ms
   */
  constructor({
    useGlobalLoding,
    errorBackCaller,
    interceptors,
    useRetryRequest,
    useRetryCount,
    useRetryDelay,
    ...config
  }) {
    // 创建一个axios实例
    this.instance = axios.create({ timeout: defaultConfig.DEFAULT_TIMEOUT, ...config })
    // 所有的请求队列，方便后续根据请求状态管理请求
    this.requestList = new Map()

    // 统一处理最常见的几个问题
    // 是否全局显示loading
    this.useGlobalLoding = useGlobalLoding ?? defaultConfig.USE_GLOBAL_LODING

    // 是否需要对axios的状态进行统一处理
    this.errorBackCaller = errorBackCaller ?? defaultConfig.ERROR_BACK_CALLER

    // 如果请求超时是否需要重试请求
    this.useRetryRequest = useRetryRequest ?? defaultConfig.USE_RETRY_REQUEST
    if (this.useRetryRequest) {
      this.retryCount = useRetryCount ?? defaultConfig.RETRY_COUNT
      this.retryDelay = useRetryDelay ?? defaultConfig.RETRY_DELAY
      this.questCount = 0
    }

    if (interceptors && interceptors.requestInterceptor) {
      // 将实例配置中的请求拦截注入到axios实例中
      // 实例中的拦截器必须要先注入，下面的类拦截器才会先于实例的执行
      this.instance.interceptors.request.use(
        interceptors.requestInterceptor,
        interceptors.requestCatch
      )
    }

    // 配置全局请求拦截器
    this.instance.interceptors.request.use(
      config => {
        console.log(
          '统一的请求拦截成功，创建时拦截和实例拦截都在它执行完成后再次执行，和我们在这里进行一个全局的请求配置: ',
          config
        )
        // 如果需要使用全局Loading；
        if (this.useGlobalLoding) {
          showLoading({
            from: 'axios',
            text: '数据加载中...',
          })
        }

        // 因为每一个实例都可能会有不同的配置，所以这里的config还会被实例拦截器进行更多的调整
        return config
      },
      err => {
        console.log('统一的请求拦截失败')
        return Promise.reject(err)
      }
    )

    // 配置全局响应拦截器
    this.instance.interceptors.response.use(
      res => {
        console.log('统一的响应成功拦截', res)
        this.useGlobalLoding && hideLoading()

        if (res.status === 200 && res.statusText === 'OK') {
          return res.data
        }
        console.log('请求失败，地址: 错误代码: %s; 错误消息: %s', res.status, res.statusText)
        // 返回到失败数据里
        return Promise.reject(res)
      },
      err => {
        console.log('统一的响应失败拦截', err, err.toJSON())
        this.useGlobalLoding && hideLoading()

        // 将错误转交给统一错误处理程序 或者直接返回给调用者
        const errJson = err.toJSON()
        const { config } = errJson
        const currentRequest = this.requestList.get(config.method + config.url)
        // 超时错误的处理
        if (errJson.message.includes('timeout')) {
          console.log('请求超时，判断是否需要统一处理错误，判断是否需要自动重新发起请求', config)
          if (
            !config.errorBackCaller &&
            !this.errorBackCaller &&
            this.useRetryRequest &&
            this.retryCount > currentRequest.count
          ) {
            currentRequest.count += 1
            return new Promise(resolve => {
              setTimeout(() => {
                resolve(this.instance.request(config))
              }, this.retryDelay)
            })
          }
        }

        // 其它错误处理
        if (!config.errorBackCaller && !this.errorBackCaller) {
          // 如果调用者需要自行处理，直接返回，否则返回处理过后的错误
          return Promise.reject(defaultConfig.errorHandler(errJson))
        } else {
          return Promise.reject(errJson)
        }
      }
    )

    // 将实例配置中的响应拦截注入到axios实例中
    // 响应的拦截器与请求拦截器相反，需要晚于类的拦截器注入时机
    if (interceptors && interceptors.responseInterceptor) {
      this.instance.interceptors.response.use(
        interceptors.responseInterceptor,
        interceptors.responseCatch
      )
    }
  }

  /**
   * 针对单个请求的处理、拦截
   * 具体的config内容参考axios官方文档，我们这里主要是一些在当前应用中需要的内容
   * @param config
   * @returns Promise
   */
  request(config) {
    // 因为需要管理请求队列，所以这里需要使用promise
    return new Promise((resolve, reject) => {
      // 注意，因为执行优先级的关系，单一请求是没办法在实例拦截器之后运行的，所以只能将拦截器的配置注入到config中，由统一拦截器进行处理
      const { method, url, data, params, cancelToken } = config
      // 使用方法和url拼成一个key
      const requestKey = method + url

      // 判断调用时是否有传入cancelToken
      // 如果candelToken已经存在，我们就没办法在这里取消请求了，所以，需要跳过这段代码
      if (!cancelToken) {
        const requestParams =
          method === 'get' || method === 'delete' ? JSON.stringify(params) : JSON.stringify(data)

        const requestInfo = {
          requestParams,
          count: 0, // 这个次数是用于处理超时重连的次数
        }

        // 创建取消请求的方法
        config.cancelToken = new CancelToken(cancel => {
          // 判断之前是否有过相同请求
          if (this.requestList.has(requestKey)) {
            const currentRequest = this.requestList.get(requestKey)
            // 需要判断参数是否一致
            if (currentRequest.requestParams === requestParams) {
              // 取消上一个请求
              currentRequest.cancel('请求频率高于设定时间，取消上一个请求')
            }
          }
          requestInfo.cancel = cancel
        })

        // 将当前请求写入队列
        this.requestList.set(requestKey, requestInfo)
      }

      // 开始调用请求
      this.instance
        .request(config)
        // 走到这里已经是通过了所有的拦截器处理过了
        .then(res => {
          // 请求结束，从队列中去除对应的请求
          this.requestList.delete(requestKey)
          //
          resolve(res)
        })
        .catch(errJson => {
          // 除超时外，其它所有错误直接从队列中删除本次请求
          this.requestList.delete(requestKey)
          //
          reject(errJson)
        })
    })
  }
}
